﻿#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Drawing;
using System.IO;


using PaintDotNet;
using PaintDotNet.Effects;

namespace PdfCreative.Filters.ConvertToTransparent
{
    /// <summary>
    /// The Filter class contains the logic for the 'convert to transparent' filter.
    /// It acts on a Paint.NET surface or a single pixel, and converts pixels
    /// to transparent pixels, using their 'lightness' as a key to transparency.
    /// </summary>
    public class Filter
    {
        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Executes the filter against a source Paint.NET surface, ouputting the result
        /// onto a destination surface. Only the specified rectangle will be affected.
        /// </summary>
        /// <param name="source">The source surface.</param>
        /// <param name="destination">The destination surface.</param>
        /// <param name="rect">The rectangle to affect.</param>
        public void Execute(Surface source, Surface destination, Rectangle rect)
        {
            for (int y = rect.Top; y < rect.Bottom; ++y)
            {
                for (int x = rect.Left; x < rect.Right; ++x)
                {
                    var rgb = source[x, y];
                    destination[x, y] = Execute(rgb);
                }
            }

        }

        /// <summary>
        /// Executes the filter against a single pixel. This method converts the specified ARGB value
        /// into one that uses the Alpha component to indicate transparency. The source ARGB value's
        /// Alpha component is ignored.
        /// </summary>
        /// <param name="argb">The ARGB value.</param>
        /// <returns>The converted value.</returns>
        public uint Execute(uint argb)
        {
            var rgb = ReadRgb(argb);

            var hsv = ConvertRgbToHsv(rgb);

            if (Double.IsNaN(hsv.H))
            {
                var shade = 1-hsv.V;
                var grey = ConvertHsvToRgb(new Hsv { H = hsv.H, S = 0, V = 0 });
                grey.Alpha = shade;
                return ReadUInt(grey);
            }
            else
            {
                var alpha = hsv.S;

                var converted = ConvertHsvToRgb(new Hsv { H = hsv.H, S = 1, V = hsv.V });
                converted.Alpha = alpha;
                return ReadUInt(converted);
            }


        }
        #endregion



        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Executes the filter against a single Paint.NET RGBA color structure.
        /// This method converts the specified ARGB value
        /// into one that uses the Alpha component to indicate transparency. The source ARGB value's
        /// Alpha component is ignored.
        /// </summary>
        /// <param name="argb">The ARGB value.</param>
        /// <returns>The converted value.</returns>
        private ColorBgra Execute(ColorBgra argb)
        {
            var normalised = NormaliseRgb(argb);

            var hsv = ConvertRgbToHsv(normalised);

            if (Double.IsNaN(hsv.H))
            {
                var shade = 1 - hsv.V;
                var grey = ConvertHsvToRgb(new Hsv { H = hsv.H, S = 0, V = 0 });
                grey.Alpha = shade;
                return DenormaliseRgb(grey);
            }
            else
            {
                var alpha = (hsv.S * hsv.S) + (1 - hsv.S) * (1 - hsv.V);
                var saturation = 1 - ((1 - hsv.S) * alpha);
                var converted = ConvertHsvToRgb(new Hsv { H = hsv.H, S = saturation, V = hsv.V});
                
                converted.Alpha = alpha;
                
                return DenormaliseRgb(converted);
            }
        }

        /// <summary>
        /// Converts a normalised RGB value to a HSV value.
        /// </summary>
        /// <param name="rgb">The normalised RGB value.</param>
        /// <returns>The HSV value.</returns>
        private Hsv ConvertRgbToHsv(Argb rgb)
        {
            Hsv result = new Hsv { H = 0, S = 0, V = 0 };

            double min, max, delta;

            min = rgb.R < rgb.G ? rgb.R : rgb.G;
            min = min  < rgb.B ? min  : rgb.B;

            max = rgb.R > rgb.G ? rgb.R : rgb.G;
            max = max  > rgb.B ? max  : rgb.B;

            result.V = max;                                // v
            delta = max - min;
            if( max > 0.0 ) {
                result.S = (delta / max);                  // s
            } else {
                // r = g = b = 0                        // s = 0, v is undefined
                result.S = 0.0;
                result.H = Double.NaN;                            // its now undefined
                return result;
            }
            if( rgb.R >= max )                           // > is bogus, just keeps compilor happy
                result.H = ( rgb.G - rgb.B ) / delta;        // between yellow & magenta
            else
            if( rgb.G >= max )
                result.H = 2.0 + ( rgb.B - rgb.R ) / delta;  // between cyan & yellow
            else
                result.H = 4.0 + ( rgb.R - rgb.G ) / delta;  // between magenta & cyan

            result.H *= 60.0;                              // degrees

            if( result.H < 0.0 )
                result.H += 360.0;

            return result;
        }

        /// <summary>
        /// Converts a HSV value to a normalised RGB value.
        /// </summary>
        /// <param name="hsv">The HSV value.</param>
        /// <returns>The normalised RGB value.</returns>
        private Argb ConvertHsvToRgb(Hsv hsv)
        {
            double hh, p, q, t, ff;
            long i;
            Argb result = new Argb { };

            if (hsv.S <= 0.0)
            {       // < is bogus, just shuts up warnings
                result.R = hsv.V;
                result.G = hsv.V;
                result.B = hsv.V;
                return result;
            }

            hh = hsv.H;
            if (hh >= 360.0) hh = 0.0;
            hh /= 60.0;
            i = (long)hh;
            ff = hh - i;
            p = hsv.V * (1.0 - hsv.S);
            q = hsv.V * (1.0 - (hsv.S * ff));
            t = hsv.V * (1.0 - (hsv.S * (1.0 - ff)));

            switch (i)
            {
                case 0:
                    result.R = hsv.V;
                    result.G = t;
                    result.B = p;
                    break;
                case 1:
                    result.R = q;
                    result.G = hsv.V;
                    result.B = p;
                    break;
                case 2:
                    result.R = p;
                    result.G = hsv.V;
                    result.B = t;
                    break;

                case 3:
                    result.R = p;
                    result.G = q;
                    result.B = hsv.V;
                    break;
                case 4:
                    result.R = t;
                    result.G = p;
                    result.B = hsv.V;
                    break;
                case 5:
                default:
                    result.R = hsv.V;
                    result.G = p;
                    result.B = q;
                    break;
            }

            return result;
        }

        /// <summary>
        /// De-normalises a source ARGB structure into a
        /// Paint.NET ARGB colour that can be stored into a destination surface.
        /// </summary>
        /// <param name="source">The ARGB value.</param>
        /// <returns>The de-normalised colour.</returns>
        private ColorBgra DenormaliseRgb(Argb source)
        {
            return new ColorBgra
            {
                A = (byte)(source.Alpha * 255),
                R = (byte)(source.R * 255),
                G = (byte)(source.G * 255),
                B = (byte)(source.B * 255)
            };
        }

        /// <summary>
        /// Normalises a source Paint.NET ARGB colour into an ARGB structure
        /// that can be converted to HSV.
        /// </summary>
        /// <param name="source">The source colour.</param>
        /// <returns>The normalised ARGB value.</returns>
        private Argb NormaliseRgb(ColorBgra source)
        {
            return new Argb
            {
                Alpha = source.A / 255d,
                R = source.R / 255d,
                G = source.G / 255d,
                B = source.B / 255d
            };
        }

        /// <summary>
        /// Reads a normalised ARGB value from an unsigned integer.
        /// </summary>
        /// <param name="argb">The unsigned integer containing an ARGB value.</param>
        /// <returns>The normalised ARGB value.</returns>
        private Argb ReadRgb(uint argb)
        {
            return new Argb
            {
                Alpha = ((argb & 0xff000000) >> 24) / 255d,
                R = ((argb & 0x00ff0000) >> 16) / 255d,
                G = ((argb & 0x0000ff00) >> 8) / 255d,
                B = (argb & 0x000000ff) / 255d
            };
        }

        /// <summary>
        /// Reads an unsigned integer from a normalised ARGB value.
        /// </summary>
        /// <param name="rgb">The RGB value.</param>
        /// <returns>The unsigned integer.</returns>
        private uint ReadUInt(Argb rgb)
        {
            return
                ((uint)(rgb.Alpha * 255) << 24) |
                ((uint)(rgb.R * 255) << 16) |
                ((uint)(rgb.G * 255) << 8) |
                ((uint)(rgb.B * 255));
        }
        #endregion

    } // Filter class
}
